﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace FrameWork
{
   public enum Loglevel
   {
       Error,
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“Loglevel.Error”的 XML 注释
       Debug,
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“Loglevel.Debug”的 XML 注释
       Winfo
   }
   public class TimHttp
   {
       public enum Method
       {
           Get,
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttp.Method.Get”的 XML 注释
           Post,
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttp.Method.Post”的 XML 注释
           Head
       }

       public TimHttp()
       {
            ServicePointManager.DefaultConnectionLimit = 1024;
            ServicePointManager.ServerCertificateValidationCallback = ValidateServerCertificate;
        }

        /// <summary>
        /// 序列化为Url
        /// </summary>
        /// <param name="param">参数</param>
        /// <param name="paramserial">是否Url编码</param>
        /// <returns></returns>
        public string ParamToUrl(Dictionary<string, string> param, bool paramserial = false)
        {
            return paramserial ? param.Aggregate("", (current, itm) => current + $"{System.Web.HttpUtility.UrlEncode(itm.Key)}={System.Web.HttpUtility.UrlEncode(itm.Value)}&").TrimEnd('&') : param.Aggregate("", (current, itm) => current + $"{itm.Key}={itm.Value}&").TrimEnd('&');
        }
        /// <summary>
        /// 序列化为Url
        /// </summary>
        /// <param name="param">参数</param>
        /// <param name="paramserial">是否Url编码</param>
        /// <returns></returns>
        public string ParamToUrl(SortedDictionary<string, object> param, bool paramserial = false)
        {
            return paramserial ? param.Aggregate("", (current, itm) => current + $"{System.Web.HttpUtility.UrlEncode(itm.Key)}={System.Web.HttpUtility.UrlEncode(itm.Value.ToString())}&").TrimEnd('&') : param.Aggregate("", (current, itm) => current + $"{itm.Key}={itm.Value.ToString()}&").TrimEnd('&');
        }
        /// <summary>
        /// 序列化为XML
        /// </summary>
        /// <param name="param">参数</param>
        /// <returns></returns>
        public string ParamToXml(Dictionary<string, string> param)
        {
            var xml = "<xml>";
            foreach (var itm in param)
            {
                xml += "<" + itm.Key + ">" + "<![CDATA[" + itm.Value + "]]></" + itm.Key + ">";
            }
            xml += "</xml>";
            return xml;
        }
        /// <summary>
        /// 序列化为XML
        /// </summary>
        /// <param name="param">参数</param>
        /// <returns></returns>
        public string ParamToXml(SortedDictionary<string, object> param)
        {
            var xml = "<xml>";
            foreach (var itm in param)
            {
                switch (itm.Value)
                {
                    case int _:
                        xml += "<" + itm.Key + ">" + itm.Value + "</" + itm.Key + ">";
                        break;
                    default:
                        xml += "<" + itm.Key + ">" + "<![CDATA[" + itm.Value + "]]></" + itm.Key + ">";
                        break;
                }
            }
            xml += "</xml>";
            return xml;
        }
        /// <summary>
        /// 序列号为Soap
        /// </summary>
        /// <param name="url"></param>
        /// <param name="method"></param>
        /// <param name="param"></param>
        /// <returns></returns>
        public string ParamToSoap(string url, Method method, Dictionary<string, object> param)
        {
            var soap = new StringBuilder();
            soap.Append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
            soap.Append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">");
            soap.Append("<soap:Body>");

            soap.Append($"<{method} xmlns=\"{new Uri(url).Host}\">");

            foreach (var itm in param)
            {
                soap.Append($"<{itm.Key}>{itm.Value}</{itm.Key}>");
            }

            soap.Append($"</{method}>");

            soap.Append("</soap:Body>");
            soap.Append("</soap:Envelope>");

            return soap.ToString();
        }
        /// <summary>
        /// 发起请求
        /// </summary>
        /// <param name="content">内容实体</param>
        /// <returns></returns>
        public string PostOrGet(ref TimHttpContent content)
        {
            var stream = GetFile(ref content);
            if (stream == null) return null;

            var reader = new StreamReader(stream, Encoding.UTF8);
            var result = reader.ReadToEnd();

            if (content.RecordInfo)
            {
                RecordInfo($"请求返回:{content.Url},类型:{content.Method},参数:{content.Param},结果:{result}", Loglevel.Debug);
            }
            return result;
        }

       public Stream GetFile(ref TimHttpContent content)
       {
            var cookiedata = content.CookieData;
            Stream result = null;

            if (content.RecordInfo)
            {
                RecordInfo($"发送请求地址:{content.Url},类型:{content.Method},参数:{content.Param}", Loglevel.Debug);
            }

            if (!string.IsNullOrWhiteSpace(content.Param) && content.Method == Method.Get)
            {
                content.Url = content.Url + "?" + content.Param;
            }

            try
            {
                if (string.IsNullOrWhiteSpace(content.Host)) content.Host = new Uri(content.Url).Host;
            }
            catch (Exception ex)
            {
                if (content.RecordInfo)
                {
                    RecordInfo(ex.Message, Loglevel.Error);
                }
            }

            HttpWebRequest request = null;
            try
            {
                request = (HttpWebRequest)WebRequest.Create(content.Url);
            }
            catch (Exception ex)
            {
                if (content.RecordInfo)
                {
                    RecordInfo(ex.Message, Loglevel.Error);
                }
                return null;
            }
            request.Method = content.Method.ToString().ToUpper();
            request.UserAgent = content.UserAgent;
            request.ContentType = content.ContentType;
            request.Accept = content.Accept;
            request.Timeout = content.Timeout;
            request.KeepAlive = content.KeepAlive;
            request.AllowAutoRedirect = content.AllowAutoRedirect;
            request.AllowWriteStreamBuffering = false;
            request.CookieContainer = new CookieContainer();
            request.Host = content.Host ?? new Uri(content.Url).Host;
            request.ServicePoint.Expect100Continue = content.Expect100Continue;

            if (!string.IsNullOrWhiteSpace(content.Referer))
            {
                request.Referer = content.Referer;
            }

            if (!string.IsNullOrWhiteSpace(content.CertFile) && !string.IsNullOrWhiteSpace(content.CertPassword))
            {
                request.ClientCertificates.Add(new X509Certificate2(content.CertFile, content.CertPassword, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable));
            }

            if (content.CertRaw != null && content.CertRaw.Length > 0 && !string.IsNullOrWhiteSpace(content.CertPassword))
            {
                request.ClientCertificates.Add(new X509Certificate2(content.CertRaw, content.CertPassword, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable));
            }

            if (!string.IsNullOrWhiteSpace(content.Agent))
            {
                var proxy = new WebProxy
                {
                    Address = new Uri("http://" + content.Agent)
                };
                request.Proxy = proxy;
            }

            if (content.Header != null && content.Header.Any())
            {
                try
                {
                    foreach (var key in content.Header.Keys)
                    {
                        var priMethod = request.Headers.GetType().GetMethod("AddWithoutValidate", BindingFlags.Instance | BindingFlags.NonPublic);
                        if (priMethod != null) priMethod.Invoke(request.Headers, new object[] { key, content.Header[key] });
                    }
                }
                catch (Exception ex)
                {
                    if (content.RecordInfo)
                    {
                        RecordInfo(ex.Message, Loglevel.Error);
                    }
                }
            }

            if (cookiedata != null && cookiedata.Count > 0)
            {
                var cookie = "";

                try
                {
                    for (var i = 0; i < cookiedata.Count; i++)
                    {
                        var itm = cookiedata[i];
                        if (string.IsNullOrWhiteSpace(itm.Name) || string.IsNullOrWhiteSpace(itm.Value)) continue;

                        request.CookieContainer.Add(itm);
                        cookie += $"{itm.Name}={itm.Value};";
                    }
                }
                catch (Exception ex)
                {
                    if (content.RecordInfo)
                    {
                        RecordInfo(ex.Message, Loglevel.Error);
                    }
                }

                request.Headers["Set-Cookie"] = cookie;
            }

            try
            {
                if (!string.IsNullOrWhiteSpace(content.Param) && content.Method == Method.Post)
                {
                    request.SendChunked = content.SendChunked;
                    var byteArray = Encoding.UTF8.GetBytes(content.Param);
                    request.ContentLength = byteArray.Length;
                    var newStream = request.GetRequestStream();
                    newStream.Write(byteArray, 0, byteArray.Length);
                    newStream.Close();
                }

                HttpWebResponse response;
                try
                {
                    response = (HttpWebResponse)request.GetResponse();
                }
                catch (WebException ex)
                {
                    response = (HttpWebResponse)ex.Response;
                }

                Stream stream;
                try
                {
                    stream = response.GetResponseStream();
                }
                catch (Exception ex)
                {
                    if (content.RecordInfo)
                    {
                        RecordInfo(ex.Message, Loglevel.Error);
                    }

                    return null;
                }

                if (stream == null) return null;

                var cookehead = response.Headers["Set-Cookie"];
                AddCookieWithCookieHead(cookehead, content.Host, ref cookiedata);
                content.CookieData = cookiedata;
                result = stream;
            }
            catch (Exception ex)
            {
                if (content.RecordInfo)
                {
                    RecordInfo(ex.Message, Loglevel.Error);
                }
            }
            return result;
        }
        private void AddCookieWithCookieHead(string cookieHead, string host, ref CookieCollection cookiedata)
        {
            if (cookieHead == null) return;
            var ary = cookieHead.Split(';');

            foreach (var ck in ary.Select(t => GetCookieFromString(t.Trim(), host)).Where(ck => ck != null))
            {
                if (cookiedata.Count == 0)
                {
                    cookiedata.Add(ck);
                }
                else
                {
                    for (var i = 0; i < cookiedata.Count; i++)
                    {
                        var itm = cookiedata[i];

                        if (itm.Name.Equals(ck.Name))
                        {
                            itm.Value = ck.Value;
                        }
                        else
                        {
                            cookiedata.Add(ck);
                        }
                    }
                }
            }
        }
        private Cookie GetCookieFromString(string cookieString, string host)
        {
            var ary = cookieString.Split(',');
            var hs = new Hashtable();
            foreach (var s in ary.Select(t => t.Trim()).Where(s => s.Split('=').Length >= 2))
            {
                var index = s.IndexOf("=", StringComparison.Ordinal);
                var key = s.Substring(0, index);
                var value = s.Substring(index + 1, s.Length - key.Length - 1);
                if (value.Equals("EXPIRED")) continue;
                hs.Add(key, value);
            }
            var ck = new Cookie();
            foreach (var key in hs.Keys)
            {
                switch (key.ToString())
                {
                    case "path":
                        ck.Path = hs[key].ToString();
                        break;
                    case "expires":
                        break;
                    case "domain":
                        ck.Domain = hs[key].ToString();
                        break;
                    default:
                        ck.Name = key.ToString();
                        ck.Value = hs[key].ToString();
                        break;
                }
            }
            if (string.IsNullOrWhiteSpace(ck.Name)) return null;

            if (string.IsNullOrWhiteSpace(ck.Domain) && !string.IsNullOrWhiteSpace(host)) ck.Domain = host;

            return ck;
        }
        private void RecordInfo(string info, Loglevel level)
        {
            var filepath = BaseDirectory() + "/log/";

            if (!Directory.Exists(filepath))
            {
                Directory.CreateDirectory(filepath);
            }
            var str = new StringBuilder();
            str.AppendLine("------------------------------------------------------");
            str.AppendLine($"时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss:fff}");
            str.AppendLine($"日志:{info}");

            try
            {
                if (level == Loglevel.Error)
                {
                    File.AppendAllText($"{filepath}{DateTime.Now:yyyy.MM.dd}_error.log", str.ToString(), Encoding.UTF8);
                }
                if (level == Loglevel.Debug)
                {
                    File.AppendAllText($"{filepath}{DateTime.Now:yyyy.MM.dd}_debug.log", str.ToString(), Encoding.UTF8);
                }
                if (level == Loglevel.Winfo)
                {
                    File.AppendAllText($"{filepath}{DateTime.Now:yyyy.MM.dd}_winfo.log", str.ToString(), Encoding.UTF8);
                }
            }
            catch (Exception)
            {
                // ignored
            }
        }
        private string BaseDirectory()
        {
            return AppDomain.CurrentDomain.BaseDirectory;
        }
        private bool ValidateServerCertificate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error)
        {
            return true;
        }
    }
    /// <summary>
    /// 内容实体
    /// </summary>
    public class TimHttpContent
    {
       public string UserAgent { get; set; } = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36";
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.UserAgent”的 XML 注释
       public string Accept { get; set; } = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8";
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.Accept”的 XML 注释
       public string ContentType { get; set; } = "application/x-www-form-urlencoded; charset=UTF-8";
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.ContentType”的 XML 注释
       public string Url { get; set; }
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.Url”的 XML 注释
       public string Host { get; set; }
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.Host”的 XML 注释
       public bool KeepAlive { get; set; }
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.KeepAlive”的 XML 注释
       public bool SendChunked { get; set; } = false;
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.SendChunked”的 XML 注释
       public bool AllowAutoRedirect { get; set; } = false;
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.AllowAutoRedirect”的 XML 注释
       public string Referer { get; set; }
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.Referer”的 XML 注释
       public string CertFile { get; set; }
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.CertFile”的 XML 注释
       public byte[] CertRaw { get; set; }
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.CertRaw”的 XML 注释
       public string CertPassword { get; set; }
       /// <summary>
        /// 代理
        /// </summary>
        public string Agent { get; set; }

        /// <summary>
        /// 是否记录日志
        /// </summary>
        public bool RecordInfo { get; set; } = false;
       public TimHttp.Method Method { get; set; } = TimHttp.Method.Get;
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.Method”的 XML 注释
       public bool Expect100Continue { get; set; } = false;
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.Expect100Continue”的 XML 注释
       public CookieCollection CookieData { get; set; }
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.CookieData”的 XML 注释

       public Dictionary<string, string> Header { get; set; }
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.Header”的 XML 注释

       public string Param { get; set; }
#pragma warning restore CS1591 // 缺少对公共可见类型或成员“TimHttpContent.Param”的 XML 注释

       public int Timeout { get; set; } = 5000;
   }
}
